Skip to content

feat: /model slash command with native Discord autocomplete#186

Closed
sh14y wants to merge 5 commits intoopenabdev:mainfrom
sh14y:feat/model-switch-command
Closed

feat: /model slash command with native Discord autocomplete#186
sh14y wants to merge 5 commits intoopenabdev:mainfrom
sh14y:feat/model-switch-command

Conversation

@sh14y
Copy link
Copy Markdown

@sh14y sh14y commented Apr 10, 2026

Design discussion: https://discord.com/channels/1491295327620169908/1491365157010542652/1493920430283686049

Summary

Adds runtime model switching via Discord's native /model slash command, with autocomplete-driven model selection.

What changed

This PR contains three commits that build on each other:

  1. feat: add !model command for runtime model switching via session/set_model

    • Wires up the ACP session/set_model JSON-RPC method (verified on Kiro CLI v1.29.3)
    • Parses models.availableModels from session/new response
    • Adds alias resolution: auto, opus, sonnet, haiku
    • Initial text-based !model interface (superseded in commit 3 below)
  2. fix: self-bootstrap kiro creds and config.toml inside the binary

    • Required for the cloud deployment we're using to test this; restores credentials from KIRO_CRED_B64 and generates config.toml from env vars when missing. Idempotent — no-op when files already exist.
  3. feat: replace !model text command with /model slash command + autocomplete ← the main UX change

    • Removes the !model text intercept entirely
    • Registers /model as a guild command on ready() for instant propagation
    • Adds interaction_create handler for both Command and Autocomplete interactions
    • SessionPool gains a cached_models snapshot updated on every session/new, so autocomplete can answer within Discord's 3-second deadline without spawning a fresh agent (~10s cold start)
    • Background warmup task at startup populates the model cache before first use
    • Aliases (auto, opus, sonnet, haiku) appear in autocomplete as friendly shortcuts only when their target model is actually available
    • The set path defers the response since cold session creation can exceed 3s

UX

Action Result
Type /m in Discord Native command picker shows /model
Tab into the model field Autocomplete returns aliases + all available model ids (current model marked)
Pick with ↑↓, hit enter Bot defers, sets the model on the channel/thread session, replies ✅ Switched to <id>
/model with no argument Bot replies with the full list and marks the current selection

Important: bot invite scope

/model will not appear unless the bot is invited with both bot and applications.commands OAuth scopes. Existing installations with only bot scope must re-invite using a new URL — the bot account stays the same and no data is lost.

The README has been updated to call this out in the Quick Start section.

Test plan

  • cargo build --release clean
  • /model (no arg) lists all available models with current marker — verified
  • /model auto switches and replies ✅ Switched to auto — verified
  • Verify /model sonnet resolves alias and switches
  • Verify autocomplete shows live filtered suggestions as you type
  • Allowlist (channel + user) enforcement for the slash command + autocomplete path — code-level via pure allowlist_decision() + 9 unit tests (empty lists, channel in/out, thread parent in/out, user in/out, combinations). Runtime smoke test deferred to post-deploy.

…model

- Add ModelInfo struct and model state fields to AcpConnection
- Parse available models from session/new response
- Add session_set_model() and resolve_model_alias() methods
- Add !model Discord command to list and switch models
- Support aliases: opus, sonnet, haiku, auto

Kiro ACP session/set_model verified on Kiro CLI v1.29.3
@sh14y sh14y requested a review from thepagent as a code owner April 10, 2026 14:29
@sh14y sh14y force-pushed the feat/model-switch-command branch from 5494848 to caac7a3 Compare April 10, 2026 15:28
Move the entrypoint.sh logic into main.rs so openab is independent of any
external shell wrapper. Before loading config, it now:

  1. Restores ~/.local/share/kiro-cli/data.sqlite3 from KIRO_CRED_B64 if
     missing (idempotent — skips when file already exists, e.g. from a
     mounted volume).
  2. Generates the config file from DISCORD_BOT_TOKEN / DISCORD_CHANNEL_ID
     env vars when the path doesn't exist. Secrets are written as literal
     ${VAR} placeholders so they get expanded by load_config() at read
     time and never land on disk in plaintext.

Why: Zeabur deployments were intermittently running images built without
the entrypoint shim (e.g. when the source branch was switched mid-flight),
producing "failed to read /etc/openab/config.toml" with no entrypoint logs.
With the bootstrap inside the binary, the deploy is robust to any image
build path that happens to omit our shell wrapper.

The Dockerfile entrypoint.sh remains as a belt-and-suspenders no-op when
present.
@sh14y sh14y force-pushed the feat/model-switch-command branch from a4290bb to 9151206 Compare April 11, 2026 01:51
…plete

Discord's slash commands give a much better UX than text commands:
- Typing /m surfaces /model in Discord's native command picker
- Autocomplete on the model option shows the live list of available
  models (and aliases) with arrow-key selection
- Discord renders the response as an interaction reply instead of
  cluttering the channel as a regular bot message

Implementation notes:

- SessionPool gains a cached_models snapshot updated on every
  session_new(). Autocomplete must respond within Discord's 3-second
  deadline, which rules out spawning a fresh kiro-cli (~10s cold start).
- main.rs adds a background warmup task that creates one session at
  startup so the cache is populated before the first user invocation.
- /model is registered as a guild command on ready() for instant
  propagation (global commands take up to 1 hour).
- The interaction handler defers the response on the set path because
  cold session creation can exceed Discord's 3s window for non-deferred
  replies.
- README documents the applications.commands scope requirement; without
  it the slash command will not appear in Discord clients.

The !model text command is removed entirely — /model is its replacement,
not an addition.
@sh14y sh14y changed the title feat: add !model command for runtime model switching via session/set_model feat: /model slash command with native Discord autocomplete Apr 11, 2026
@thepagent
Copy link
Copy Markdown
Collaborator

Native Discord slash command with autocomplete and model caching is a nice feature but touches multiple components (Discord API registration, SessionPool, autocomplete). Could you please create a Discord discussion thread and add the link to this PR description for design tracking? Thanks! 🙏

@thepagent
Copy link
Copy Markdown
Collaborator

@thepagent
Copy link
Copy Markdown
Collaborator

Related issues worth noting alongside this PR:

The cached_models approach for beating Discord's 3-second autocomplete deadline is a clean solution. One thing still open per the test plan: allowlist enforcement (channel + user) for the slash command path — worth tracking before merge.

…omplete

- Add interaction_allowlist_ok() helper (channel, thread-parent, user checks)
- Apply to handle_model_command: reject with ephemeral reply if not allowed
- Apply to handle_model_autocomplete: return empty choices if not allowed

Addresses review feedback from thepagent on PR openabdev#186

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sh14y
Copy link
Copy Markdown
Author

sh14y commented Apr 15, 2026

Thanks for the review! I've addressed both points:

Extract pure allowlist logic into ctx-free allowlist_decision() so it can be
unit-tested without a Discord runtime. interaction_allowlist_ok() becomes a
thin wrapper that resolves the thread parent via HTTP and delegates.

Covers: empty lists, channel in/out, thread parent in/out, user in/out, and
the "thread-allowed + user-denied" combination.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sh14y
Copy link
Copy Markdown
Author

sh14y commented Apr 15, 2026

Follow-up: extracted the allowlist logic into a pure allowlist_decision() function and added 9 unit tests covering:

  • Empty allowlists allow everything
  • Channel in/out of allowlist
  • Thread parent in/out of allowlist
  • User in/out of allowlist
  • Combinations (e.g. thread-allowed + user-denied)

interaction_allowlist_ok() is now a thin wrapper that resolves the thread parent via HTTP and delegates to the pure function. All 9 tests pass via cargo test. Runtime verification against a live Discord instance is deferred until post-deploy — the logic is identical to the existing message() path which has been running in prod.

@chaodu-agent
Copy link
Copy Markdown
Collaborator

四法師 Collaborative Triage Review — PR #186 (2026-05-01)

Verdict: SUPERSEDED by #478 — The /model slash command feature this PR implements has been fully superseded by PR #478 (feat: /models, /agents, and /cancel slash commands with ACP configOptions), which is already merged to main.

🟢 INFO — What was good about this PR
🔴 SUPERSEDED — Why this PR should be closed

1. Feature fully covered by #478 (merged)

Current main already has:

  • /models — select AI model from dropdown (ephemeral), using ACP configOptions protocol
  • /agents — select agent mode from dropdown
  • /cancel — cancel in-flight operations

PR #186's /model command is a subset of this. Key differences:

Aspect PR #186 #478 (on main)
Protocol Custom session/set_model Standard ACP configOptions
Backend support Kiro-specific aliases Any ACP backend (Kiro, Claude, Codex, Cursor)
Commands /model only /models + /agents + /cancel
Model list models.availableModels from session/new configOptions from ACP protocol
Aliases Hardcoded opus/sonnet/haiku No hardcoded aliases (protocol-driven)

2. Based on extremely old main

The PR predates:

Rebasing would require rewriting essentially the entire PR.

3. Bootstrap functions are out of scope

bootstrap_kiro_credentials() and bootstrap_config() are Kiro-specific deployment concerns unrelated to the /model feature. If still needed, they should be a separate PR.

📋 Recommendation

Close this PR with a thank-you to @sh14y for the original work. The core feature is live on main via #478.

If any net-new value is worth preserving:


Reviewed by 超渡法師 on behalf of the 四法師 triage team.

Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 SUPERSEDED — Fully superseded by PR #478 (merged). Recommend close.

Baseline Check (Step 0)
Field Value
State OPEN
Mergeable CONFLICTING
Created 2026-04-10 (21 days ago)
Changes +615 / -7 across 7 files
Labels closing-soon

Main branch status: /models slash command (select menu UI), /agents, /cancel, /reset — all using standard ACP configOptions protocol via PR #478 (merged 2026-04-19). More general implementation that works with any ACP backend.

Four-Question Framework

1. What problem does it solve?
Runtime model switching via Discord's native /model slash command with autocomplete.

2. How does it solve it?

  • Parses models.availableModels from session/new ACP response
  • ModelInfo struct + cached_models on SessionPool for fast autocomplete
  • Registers /model as guild command with autocomplete-enabled string option
  • Hardcoded aliases (auto, opus, sonnet, haiku)
  • Background warmup task populates model cache at startup
  • Also bundles Kiro credential bootstrap (KIRO_CRED_B64)

3. What was considered?
Started as text-based !model command (PR #185), evolved to native slash command. Cached model list to beat Discord's 3s autocomplete deadline.

4. Is it the best approach?
No longer relevant. Main uses standard ACP configOptions protocol (PR #478) which is protocol-driven, backend-agnostic, and covers /models + /agents + /cancel in one implementation.

Traffic Light

🟢 INFO

  • allowlist_decision() pure function with 9 unit tests — excellent pattern (later adopted in main)
  • Cached model list to solve 3s autocomplete deadline — clever engineering
  • Thorough PR description

🔴 SUPERSEDED

  1. Feature fully covered by #478 (merged) — Main has /models with select menu using standard ACP configOptions. More general, more backends supported.
  2. Merge conflicts — Based on pre-refactor architecture missing ~6 major changes.
  3. Hardcoded aliases (opusclaude-opus-4.6) will break as model versions change. Main's protocol-driven approach avoids this.
  4. Bootstrap functions out of scopebootstrap_kiro_credentials() and bootstrap_config() are unrelated to /model feature.
Aspect PR #186 Current main
Model switching Custom session/set_model ACP configOptions (PR #478)
Commands /model only /models + /agents + /cancel + /reset
Aliases Hardcoded 4 aliases Protocol-driven, no hardcoded aliases
Architecture Direct pool access ChatAdapter trait + AdapterRouter

Recommendation: Close this PR. Every feature it introduces either already exists on main in a better form or is out of scope.

Reviewed by 超渡法師 🔃 chaodu Backlog triage

@chaodu-agent
Copy link
Copy Markdown
Collaborator

Closing — fully superseded by PR #478 (merged), which implements /models, /agents, /cancel, and /reset using the standard ACP configOptions protocol. The protocol-driven approach is more general and backend-agnostic. Thanks for the pioneering work on model switching! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

closing-soon PR missing Discord Discussion URL — will auto-close in 3 days pending-contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants